#!/usr/bin/env python
"""检测 OpenAPI 规范与当前代码的漂移.

此脚本对比固化的 OpenAPI 契约（backend/openapi/openapi.json）
与运行时生成的规范，检测是否有端点增删或结构变化。

用法:
    python scripts/check_openapi_drift.py

要求:
    - 后端服务器必须运行在 http://localhost:8000
    - backend/openapi/openapi.json 文件必须存在

返回值:
    0: 无漂移，契约同步
    1: 检测到漂移或错误
"""

from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Any

try:
    import requests
except ImportError:
    print("[ERROR] Missing 'requests' library. Install with: pip install requests")
    sys.exit(1)


def load_contract_spec(contract_path: Path) -> dict[str, Any] | None:
    """加载固化的契约规范."""
    if not contract_path.exists():
        print(f"[FAIL] Contract file not found: {contract_path}")
        print("[INFO] Generate it with:")
        print("  python scripts/check_openapi_drift.py --generate")
        return None

    try:
        with open(contract_path, encoding="utf-8") as f:
            return json.load(f)
    except json.JSONDecodeError as e:
        print(f"[FAIL] Invalid JSON in contract file: {e}")
        return None


def fetch_current_spec(base_url: str) -> dict[str, Any] | None:
    """获取当前运行时的 OpenAPI 规范."""
    try:
        response = requests.get(f"{base_url}/api/v1/openapi.json", timeout=10)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.ConnectionError:
        print(f"[FAIL] Cannot connect to {base_url}")
        print("[INFO] Make sure the backend is running: make dev")
        return None
    except requests.exceptions.Timeout:
        print(f"[FAIL] Request timeout to {base_url}")
        return None
    except requests.exceptions.HTTPError as e:
        print(f"[FAIL] HTTP error: {e}")
        return None
    except json.JSONDecodeError as e:
        print(f"[FAIL] Invalid JSON response: {e}")
        return None


def compare_paths(contract: dict[str, Any], current: dict[str, Any]) -> tuple[set[str], set[str]]:
    """对比路径差异，返回 (新增, 删除)."""
    contract_paths = set(contract.get("paths", {}).keys())
    current_paths = set(current.get("paths", {}).keys())

    added = current_paths - contract_paths
    removed = contract_paths - current_paths

    return added, removed


def compare_methods(contract: dict[str, Any], current: dict[str, Any]) -> list[str]:
    """对比每个路径的 HTTP 方法变化."""
    changes = []
    contract_paths = contract.get("paths", {})
    current_paths = current.get("paths", {})

    common_paths = set(contract_paths.keys()) & set(current_paths.keys())

    for path in common_paths:
        contract_methods = set(contract_paths[path].keys())
        current_methods = set(current_paths[path].keys())

        added_methods = current_methods - contract_methods
        removed_methods = contract_methods - current_methods

        if added_methods:
            changes.append(f"  {path}: added methods {sorted(added_methods)}")
        if removed_methods:
            changes.append(f"  {path}: removed methods {sorted(removed_methods)}")

    return changes


def check_drift(base_url: str = "http://localhost:8000", contract_path: Path | None = None) -> int:
    """主检测函数."""
    if contract_path is None:
        contract_path = Path("backend/openapi/openapi.json")

    print("[INFO] Checking OpenAPI specification drift...")
    print(f"[INFO] Contract file: {contract_path}")
    print(f"[INFO] Backend URL: {base_url}")
    print()

    # 加载固化契约
    contract_spec = load_contract_spec(contract_path)
    if contract_spec is None:
        return 1

    # 获取当前规范
    current_spec = fetch_current_spec(base_url)
    if current_spec is None:
        return 1

    # 对比路径
    added_paths, removed_paths = compare_paths(contract_spec, current_spec)

    # 对比方法
    method_changes = compare_methods(contract_spec, current_spec)

    # 报告结果
    has_drift = bool(added_paths or removed_paths or method_changes)

    if has_drift:
        print("[FAIL] ⚠️  OpenAPI spec drift detected!")
        print()

        if added_paths:
            print(f"[+] Added {len(added_paths)} path(s):")
            for path in sorted(added_paths):
                print(f"    + {path}")
            print()

        if removed_paths:
            print(f"[-] Removed {len(removed_paths)} path(s):")
            for path in sorted(removed_paths):
                print(f"    - {path}")
            print()

        if method_changes:
            print(f"[~] Modified {len(method_changes)} path(s):")
            for change in method_changes:
                print(f"    {change}")
            print()

        print("[INFO] To update the contract:")
        print(f"  curl {base_url}/api/v1/openapi.json > {contract_path}")
        print()
        print("Or use Python:")
        print("  python scripts/check_openapi_drift.py --generate")
        return 1

    print("[PASS] ✓ OpenAPI spec is in sync with code")
    print(f"[INFO] Total endpoints: {len(contract_spec.get('paths', {}))}")
    return 0


def generate_contract(base_url: str = "http://localhost:8000", output_path: Path | None = None) -> int:
    """生成并保存契约文件."""
    if output_path is None:
        output_path = Path("backend/openapi/openapi.json")

    print(f"[INFO] Generating OpenAPI contract from {base_url}...")

    current_spec = fetch_current_spec(base_url)
    if current_spec is None:
        return 1

    # 确保目录存在
    output_path.parent.mkdir(parents=True, exist_ok=True)

    # 保存契约
    try:
        with open(output_path, "w", encoding="utf-8") as f:
            json.dump(current_spec, f, indent=2, ensure_ascii=False)

        print(f"[OK] Contract saved to: {output_path}")
        print(f"[OK] Total endpoints: {len(current_spec.get('paths', {}))}")
        print(f"[OK] API version: {current_spec.get('info', {}).get('version')}")
        print()
        print("[INFO] Don't forget to commit the contract:")
        print(f"  git add {output_path}")
        print('  git commit -m "feat: update OpenAPI contract"')
        return 0
    except OSError as e:
        print(f"[FAIL] Cannot write contract file: {e}")
        return 1


def main() -> int:
    """主入口."""
    import argparse

    parser = argparse.ArgumentParser(
        description="Check OpenAPI specification drift",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "--generate",
        "-g",
        action="store_true",
        help="Generate and save contract from running server",
    )
    parser.add_argument(
        "--url",
        default="http://localhost:8000",
        help="Backend base URL (default: http://localhost:8000)",
    )
    parser.add_argument(
        "--contract",
        type=Path,
        default=Path("backend/openapi/openapi.json"),
        help="Path to contract file (default: backend/openapi/openapi.json)",
    )

    args = parser.parse_args()

    if args.generate:
        return generate_contract(args.url, args.contract)
    else:
        return check_drift(args.url, args.contract)


if __name__ == "__main__":
    sys.exit(main())
